﻿using MongoDB.Driver;
using SimpleInjector;
using Sustainalytics.AwsS3.Client;
using Sustainalytics.ClientDW.Entities;
using Sustainalytics.CompanyReportSyncStep.Exceptions;
using Sustainalytics.DataLayer;
using Sustainalytics.DataLayer.Shared;
using Sustainalytics.Entities;
using Sustainalytics.Entities.ConnectionStrings;
using Sustainalytics.Entities.ESGRatings;
using Sustainalytics.ESG.Entities;
using Sustainalytics.ESGRatings.BusinessLogic.ESGOnlineReport;
using Sustainalytics.Utils;
using Sustainalytics.Utils.Configuration;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;


namespace Sustainalytics.ReportGenerator.BL
{
    // load data from db colletions in bulk
    public class PdfReportGeneration
    {
        private readonly int _bulkSize;
        private readonly Dictionary<string, string> _connectionString;
        private string _clientDwMasterConnectionstring;

        private ConcurrentBag<RenderResult> _renderResults;
        private ConcurrentQueue<string> _filesToBeDeleted;
        private ConcurrentDictionary<string, IGrouping<string, FileInfo>> _previousFiles;
        private readonly Guid[] _controverseyAffectedCompanyIds;
        private readonly Guid[] _requestedCompanies;

        private ConcurrentQueue<Guid> _esgFailures;
        private ConcurrentQueue<Guid> _controversyFailures;
        private int _esgReportsCounter;
        private int _controversyReportsCounter;

        private ActionBlock<Helper> _consumer;
        private BufferBlock<Helper> _producer;

        private ReportGenerator _reportGenerator;

        private DatabaseGroup _databaseGroup;
        private DateTime _generationDate;

        private readonly Action<string> _logAction;
        private IS3Client _s3client;
        private CancellationToken _token;
     
        private void PerformLog(string msg)
        {
            if (_logAction == null) return;
            _logAction.Invoke(msg);
        }


        private PdfReportGeneration()
        { }
            

        public PdfReportGeneration(Action<string> mAction, Dictionary<string, string> connections, CancellationToken token, Guid[] controverseyAffectedCompanyIds, Guid[] requestedCompanies) : this()
        {
            _controverseyAffectedCompanyIds = controverseyAffectedCompanyIds;
            _requestedCompanies = requestedCompanies ?? new Guid[0];
            _bulkSize = 100;
            _connectionString = connections;
            _logAction = mAction;
            _token = token;
        }

        public PdfReportGeneration(int bulkSize, Dictionary<string, string> connections, CancellationToken token, Action<string> maAction = null): this()
        {
            if (bulkSize <= 3)
                bulkSize = 3;
            _connectionString = connections;
            _bulkSize = bulkSize;
            _token = token;
        }


        public void Start(Container container)
        {
            _esgFailures = new ConcurrentQueue<Guid>();
            _controversyFailures = new ConcurrentQueue<Guid>();
            _renderResults = new ConcurrentBag<RenderResult>();
            _filesToBeDeleted = new ConcurrentQueue<string>();


            PerformLog(DateTime.UtcNow.ToString("T")+":PdfReportGeneration.Start  _bulkSize = " + _bulkSize);
            string clientDwConnectionString,
                   clientPlatformConnectionstring,
                   dwArchiveConnectionString,
                   companyReportPdfConnectionString,
                   controversyReportPdfConnectionString;


            if (!_connectionString.TryGetValue(ConnectionStringType.ClientDW.ToString(), out clientDwConnectionString))
                throw new ArgumentException("ClientDW key is missing!");


            if (!_connectionString.TryGetValue(ConnectionStringType.ClientPlatform.ToString(), out clientPlatformConnectionstring))
                throw new ArgumentException("ClientPlatform key is missing!");


            if (!_connectionString.TryGetValue(ConnectionStringType.DWArchive.ToString(), out dwArchiveConnectionString))
                throw new ArgumentException("DWArchive key is missing!");


            if (!_connectionString.TryGetValue(ConnectionStringType.ControversyReportPDF.ToString(), out controversyReportPdfConnectionString))
                throw new ArgumentException("controversyReportPdfConnectionString key is missing!");

            if (!_connectionString.TryGetValue(ConnectionStringType.CompanyReportPDF.ToString(), out companyReportPdfConnectionString))
                throw new ArgumentException("CompanyReportPDF key is missing!");

            _clientDwMasterConnectionstring = clientPlatformConnectionstring.Replace("ClientPlatform", "ClientDWMaster");

            //as requested
            var defaultEsgProfile = (container.GetInstance<IRepositoryCollection<ESGProfile>>())
                                        .ReadAllWhereFieldEqualsValue("Name", "Default", null, 1)
                                        .SingleOrDefault();

            if (defaultEsgProfile == null)
            {
                PerformLog(DateTime.UtcNow.ToString("T")+":Default ESG Profile: not found");
                throw new DefaultProfileNotFoundException("default profile not found");
            }

            PerformLog(DateTime.UtcNow.ToString("T")+":Default ESG Profile: " + defaultEsgProfile);

            _generationDate = DateTime.UtcNow.Date;
            SetDatabaseGroup();

            var maxDegreeOfParallelism = ParallelHelper.GetMaxDegreeOfParallelism();
            PerformLog(DateTime.UtcNow.ToString("T")+":generate pdfs in cpu's: " + maxDegreeOfParallelism);

            _reportGenerator = new ReportGenerator();

            RepositoryCollection<RenderResult> companyReportRepository = new RepositoryCollection<RenderResult>(companyReportPdfConnectionString, "CompanyReportPDF");
            RepositoryCollection<RenderResult> controversyReportRepository = new RepositoryCollection<RenderResult>(controversyReportPdfConnectionString, "ControversyReportPDF");

            var bulkDataReader = new GeneratePdfBulkReader(container.GetInstance<IRepositoryCollection<SubPeerGroupKeyEsgIssue>>(),
                container.GetInstance<IRepositoryCollection<Analyst>>(),
                container.GetInstance<IRepositoryCollection<IndicatorTemplate>>(),
                container.GetInstance<IRepositoryCollection<CorporateData>>(),
                container.GetInstance<IRepositoryCollection<FinancialData>>(),
                container.GetInstance<IRepositoryCollection<RPCompanyProfile>>(),
                container.GetInstance<IRepositoryCollection<RPCompanyESGComputationResult>>(),
                container.GetInstance<IHistoricalDataBusinessLogicUnit>(), 
                container.GetInstance<IRepositoryCollection<RPIndicatorExtended>>(),
                container.GetInstance<IRepositoryCollection<RPControversyIndicator>>(),
                container.GetInstance<IRepositoryCollection<ComparisonGroupControversy>>(),
                container.GetInstance<IRepositoryCollection<EventIndicator>>(),
                container.GetInstance<IRepositoryCollection<Incident>>(),
                container.GetInstance<IRepositoryCollection<CompanyKeyESGIssue>>(),
                container.GetInstance<IRepositoryCollection<Source>>(),
                _bulkSize, null, clientDwConnectionString, defaultEsgProfile, PerformLog,
                                    _token, _requestedCompanies, Math.Max(1, maxDegreeOfParallelism / 2));

            _s3client = container.GetInstance<IS3Client>();

            //todo Soryn get controversies affected companies
            var affectedCompanyIds = _controverseyAffectedCompanyIds;
            PerformLog($"{affectedCompanyIds.Count()} Affected Company Profile Ids retrieved.");

            bulkDataReader.OneTimeDataRead();

            PerformLog(DateTime.UtcNow.ToString("T")+$":AllFinancialData has {bulkDataReader.AllFinancialData.Count} records");
            PerformLog(string.Format("{0}:AnalystsDictionary has {1} records", DateTime.UtcNow.ToString("T"), bulkDataReader.AnalystsDictionary.Count));
            PerformLog(string.Format("{0}:IndicatorTemplates has {1} records", DateTime.UtcNow.ToString("T"), bulkDataReader.IndicatorTemplates.Count));
            PerformLog(string.Format("{0}:SubIndustryKeyEsgIssueDictionary has {1} records", DateTime.UtcNow.ToString("T"), bulkDataReader.SubPeerGroupKeyEsgIssueDictionary.Count));


            _previousFiles = new ConcurrentDictionary<string, IGrouping<string, FileInfo>>();

            _producer = new BufferBlock<Helper>(new DataflowBlockOptions
            {
                BoundedCapacity = (int)(_bulkSize * 1.5),
                CancellationToken = _token,
            });

            _consumer = new ActionBlock<Helper>(x =>
            {
                RenderReport(x, affectedCompanyIds);


            }, new ExecutionDataflowBlockOptions
            {
                BoundedCapacity = maxDegreeOfParallelism + 1,
                MaxDegreeOfParallelism = maxDegreeOfParallelism,
                CancellationToken = _token
            });


            using (_producer.LinkTo(_consumer, new DataflowLinkOptions { PropagateCompletion = true }))
            {
                HandleCompaniesWithFocusedRatings(bulkDataReader, _producer);
                HandleCompaniesWithESGRatings(bulkDataReader, _producer);
                HandleCompaniesWithControversyOnly(bulkDataReader, _producer);

                _producer.Complete();
                _consumer.Completion.Wait();
            }


            PerformLog(_esgReportsCounter + " ESG ratings generated");
            PerformLog(_controversyReportsCounter + " controversy reports generated");

            LogCorporateDataResearch(_esgFailures, _controversyFailures);

            var result = UpdatePdfsDatabases(companyReportRepository, controversyReportRepository, 0);
            if (result != null)
            {
                result.Wait(TimeSpan.FromMinutes(15));
            }
            //delete files 

            _renderResults = null;
            _previousFiles = null;
            _filesToBeDeleted = null;

        }

        private Task UpdatePdfsDatabases(RepositoryCollection<RenderResult> companyReportRepository, RepositoryCollection<RenderResult> controversyReportRepository, int limit = 200)
        {
            if (_renderResults == null)
                throw new ArgumentException("_renderResults");

            var count = _renderResults.Count;
            if (count > limit)
            {
                return Task.Run(() =>
                {
                    var list = new List<RenderResult>();
                    try
                    {
                        while (count > 0)
                        {
                            RenderResult result;
                            if (_renderResults.TryTake(out result))
                            {
                                list.Add(result);
                                count--;
                            }
                            else
                                break;
                        }

                        foreach (var renderResult in list)
                        {
                            renderResult.FileContent = null;
                        }

                        var companyPdfs = list.Where(x => x.ReportType == ReportType.ComprehensiveReport).ToArray();
                        if (companyReportRepository != null && companyPdfs.Any())
                            companyReportRepository.CreateOrUpdateBatch(companyPdfs);

                        var contorversyPdfs = list.Where(x => x.ReportType == ReportType.ControversyReport).ToArray();
                        if (controversyReportRepository != null && contorversyPdfs.Any())
                            controversyReportRepository.CreateOrUpdateBatch(contorversyPdfs);
                    }
                    catch (Exception ex)
                    {
                        PerformLog(ex.ToString());
                        list.ForEach(x => _renderResults.Add(x));
                    }
                });
            }
            return Task.FromResult<RenderResult>(null);
        }

        private void SetDatabaseGroup()
        {
            _databaseGroup = DatabaseGroup.None;

            if (_connectionString["ClientDW"].Contains("ClientDW_A"))
                _databaseGroup = DatabaseGroup.A;
            if (_connectionString["ClientDW"].Contains("ClientDW_B"))
                _databaseGroup = DatabaseGroup.B;
        }

        private void IncrementGeneratedReport(ReportType reportType)
        {
            if (reportType == ReportType.ControversyReport)
            {
                Interlocked.Increment(ref _controversyReportsCounter);
#if DEBUG
                Console.ForegroundColor = ConsoleColor.Blue;
                Console.WriteLine(_controversyReportsCounter + " controversy reports ");
                Console.ForegroundColor = ConsoleColor.White;
#endif
            }
            else
            {
                Interlocked.Increment(ref _esgReportsCounter);
#if DEBUG
                Console.ForegroundColor = ConsoleColor.Blue;
                Console.WriteLine(_esgReportsCounter + " ESG ratings reports");
                Console.ForegroundColor = ConsoleColor.White;
#endif
            }
        }

        private void RegisterFailedReport(ReportType reportType, Guid companyId)
        {
            if (reportType == ReportType.ControversyReport)
                _controversyFailures.Enqueue(companyId);
            else
                _esgFailures.Enqueue(companyId);
        }

        private void RenderReport(Helper hcrdo, Guid[] affectedCompanyIds)
        {
            if (hcrdo.CRDO != null)
            {
                if (affectedCompanyIds.Any())
                {
                    if (hcrdo.CRDO.ReportType == ReportType.ControversyReport &&
                        affectedCompanyIds.Contains(hcrdo.CRDO.CompanyId))
                    {
                        var isSuccessfullyGenerated = GenerateReport(hcrdo.CRDO.ReportType, hcrdo, _reportGenerator);

                        if (isSuccessfullyGenerated)
                            IncrementGeneratedReport(ReportType.ControversyReport);
                        else
                            RegisterFailedReport(ReportType.ControversyReport, hcrdo.CRDO.CompanyId);
                    }
                    else
                    {
                        PerformLog(string.Format("CompanyId {0} is not contained in controversies affected company profile ids", hcrdo.CRDO.CompanyId));
                    }
                }
                else
                {
                    var isSuccessfullyGenerated = GenerateReport(hcrdo.CRDO.ReportType, hcrdo, _reportGenerator);

                    if (isSuccessfullyGenerated)
                        IncrementGeneratedReport(hcrdo.CRDO.ReportType);
                    else
                        RegisterFailedReport(hcrdo.CRDO.ReportType, hcrdo.CRDO.CompanyId);
                }

                hcrdo.CRDO = null;
            }
            else
            {
                PerformLog("Found crdo null!");
            }
        }

        private void LogCorporateDataResearch(IEnumerable<Guid> failedEsgCompaniesIds,
                                                        IEnumerable<Guid> failedControversyCompaniesIds)
        {

            PerformLog(string.Format("ESG Report generation failed  the following ESG profiles: ('{0}')",
                                            string.Join("','", failedEsgCompaniesIds.Select(cid => cid.ToString()))));

            PerformLog(string.Format("Controversy report generation failed / have empty feature universes list the following controversy profiles: ('{0}')",
                                            string.Join("','", failedControversyCompaniesIds.Select(cid => cid.ToString()))));
        }

        private void HandleCompaniesWithESGRatings(GeneratePdfBulkReader bulkDataReader, BufferBlock<Helper> producer)
        {
            for (;;)
            {
                _token.ThrowIfCancellationRequested();

                try
                {
                    var batch = bulkDataReader.NextESGRatingsBulk();
                    var companyReportDataOfflines = batch as CompanyReportDataOffline[] ?? batch.ToArray();
                    if (!companyReportDataOfflines.Any())
                        break;


                    Console.WriteLine("Loaded esgs " + companyReportDataOfflines.Count() + " " + DateTime.UtcNow.ToString("s"));

                    foreach (var crdo in companyReportDataOfflines)
                    {
                        _token.ThrowIfCancellationRequested();
                        if (crdo == null)
                        {
                            PerformLog("crdo == null");
                            continue;
                        }

                        while (!producer.Post(new Helper(crdo)))
                        {
                            _token.ThrowIfCancellationRequested();
                            Thread.Sleep(1000);
                        }
                    }
                }
                catch (Exception ex)
                {
                    PerformLog(ex.ToString());
                }
            }
        }

        private void HandleCompaniesWithFocusedRatings(GeneratePdfBulkReader bulkDataReader, BufferBlock<Helper> producer)
        {
            for (;;)
            {
                _token.ThrowIfCancellationRequested();

                try
                {
                    var batch = bulkDataReader.NextFocusedRatingsOnlyBulk();
                    var companyReportDataOfflines = batch as CompanyReportDataOffline[] ?? batch.ToArray();
                    if (!companyReportDataOfflines.Any())
                        break;

                    Console.ForegroundColor = ConsoleColor.Yellow;
                    Console.WriteLine("Loaded focused ratings " + companyReportDataOfflines.Count() + " " + DateTime.UtcNow.ToString("s"));
                    Console.ForegroundColor = ConsoleColor.White;

                    foreach (var crdo in companyReportDataOfflines)
                    {
                        _token.ThrowIfCancellationRequested();
                        if (crdo == null)
                        {
                            PerformLog("crdo == null");
                            continue;
                        }

                        while (!producer.Post(new Helper(crdo)))
                        {
                            _token.ThrowIfCancellationRequested();
                            Thread.Sleep(1000);
                        }
                    }
                }
                catch (Exception ex)
                {
                    PerformLog(ex.ToString());
                }
            }
        }

        private void HandleCompaniesWithControversyOnly(GeneratePdfBulkReader bulkDataReader, BufferBlock<Helper> producer)
        {
            for (;;)
            {
                try
                {
                    _token.ThrowIfCancellationRequested();

                    var batch = bulkDataReader.NextControversyOnlyBulk();
                    var companyReportDataOfflines = batch as CompanyReportDataOffline[] ?? batch.ToArray();
                    if (!companyReportDataOfflines.Any())
                        break;


                    Console.WriteLine("Loaded controversy " + companyReportDataOfflines.Count() + " " + DateTime.UtcNow.ToString("s"));

                    foreach (var crdo in companyReportDataOfflines)
                    {
                        _token.ThrowIfCancellationRequested();
                        if (crdo == null)
                        {
                            PerformLog("crdo == null");
                            continue;
                        }

                        while (!producer.Post(new Helper(crdo)))
                        {
                            _token.ThrowIfCancellationRequested();
                            Thread.Sleep(1000);
                        }
                    }

                }
                catch (Exception ex)
                {
                    PerformLog(ex.ToString());
                }
            }
        }


        private bool GenerateReport(ReportType reportType, Helper hcrdo, IReportGenerator reportGenerator)
        {
            var isOk = true;
            var fileName = string.Empty;
            var companyId = hcrdo.CRDO.CompanyId;

            try
            {
                var result = reportGenerator.GenerateReportPdf(hcrdo.CRDO, false, true);
                var generationTime = result[0].RenderDuration;
                fileName = result[0].FileName;


                WriteFilePdf(result[0]);
                UpdatePregeneratedReport(companyId, fileName, reportType, true, null, generationTime);

                if (reportType == ReportType.CoreReport ||
                    reportType == ReportType.ComprehensiveReport)
                {
                    WriteFilePdf(result[1]);
                    UpdatePregeneratedReport(companyId, fileName, hcrdo.CRDO.ReportType, true, null, generationTime);
                }
            }
            catch (Exception ex)
            {
                UpdatePregeneratedReport(companyId, fileName, reportType, false, ex.ToString(), TimeSpan.Zero);

                isOk = false;
            }

            return isOk;
        }


        private void WriteFilePdf(RenderResult renderResult)
        {
            try
            {
                if (renderResult == null) return;

                var path = $"{renderResult.ReportType}_{_databaseGroup}/{renderResult.FileName}";
                
                S3Write(renderResult, path);

                renderResult.FileContent = null;
                _renderResults.Add(renderResult);
            }
            catch (Exception ex)
            {
                PerformLog(ex.ToString());
            }
        }

        private void S3Write(RenderResult renderResult, string fullPath)
        {
            var bucketName = SettingsHelper.Read("s3bucket", "dev-globalaccess-europe");
            var file = string.Format("{0}/{1}", Path.GetFileName(Path.GetDirectoryName(fullPath)), renderResult.FileName);
            PerformLog(string.Format(DateTime.UtcNow.ToString("T")+":Uploading file {0} in S3 {1} bucket", file, bucketName));
            _s3client.UploadFile(bucketName, file, renderResult.FileContent);
        }

        private void UpdatePregeneratedReport(Guid companyId, string fileName, ReportType reportType, bool isSuccessfullyGenerated, string error, TimeSpan renderDuration)
        {
            var pregeneratedReport = new PregeneratedReport
            {
                Id = Guid.NewGuid(),
                CompanyId = companyId,
                ReportType = reportType.ToString(),
                DatabaseGroup = _databaseGroup.ToString(),
                GenerationDate = _generationDate,
                FileName = fileName,
                IsSuccessfullyGenerated = isSuccessfullyGenerated,
                Error = error,
                RenderDuration = renderDuration
            };

            var database = MongoDatabaseFactory.GetDatabase(_clientDwMasterConnectionstring);

            var collection = database.GetCollection<PregeneratedReport>(typeof(PregeneratedReport).Name);
            var builder = Builders<PregeneratedReport>.Filter;
            var filter = builder.Eq(pr => pr.CompanyId, companyId)
                & builder.Eq(pr => pr.ReportType, reportType.ToString())
                & builder.Eq(pr => pr.DatabaseGroup, _databaseGroup.ToString());

            var update = Builders<PregeneratedReport>.Update
                .Set(pr => pr.GenerationDate, _generationDate)
                .Set(pr => pr.FileName, fileName)
                .Set(pr => pr.IsSuccessfullyGenerated, isSuccessfullyGenerated)
                .Set(pr => pr.Error, error)
                .Set(pr => pr.RenderDuration, renderDuration);

            var result = collection.UpdateOne(filter, update);
            if (result.MatchedCount == 0)
            {
                collection.InsertOne(pregeneratedReport);
            }

            if (isSuccessfullyGenerated)
            {
                var mgs = string.Format("{0}:The {1} was generated successfully. Filename: {2}, duration:{3}", DateTime.UtcNow.ToString("T"), reportType, fileName, renderDuration);

                PerformLog(mgs);
#if DEBUG
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine(mgs);
                Console.ForegroundColor = ConsoleColor.White;
#endif
            }
            else
            {
                var mgs = string.Format("{0}:{1} generation failed! Filename: {2}", DateTime.UtcNow.ToString("T"),reportType, fileName);

                PerformLog(mgs);
#if DEBUG
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine(mgs);
                Console.ForegroundColor = ConsoleColor.White;
#endif
            }
        }

        private class Helper
        {
            public Helper(CompanyReportDataOffline crdo)
            {
                CRDO = crdo;
            }

            public CompanyReportDataOffline CRDO { get; set; }
        }
    }
}